/***
 * Based on MochiKit.Async@r1525 <http://mochikit.com/>, (c) 2005 Bob Ippolito.
 *
 * Modifications/adaptations (c) 2009 Univrz.
 */
package com.bigflexlib.async
{
	import flash.net.URLLoader;
	import flash.utils.clearTimeout;
	import flash.utils.setTimeout;

	import mx.rpc.AsyncToken;

	public class Deferred
	{
		private static var nextId:uint = 0;

		public var id:uint;
		public var chain:Array = [];
		public var fired:int = -1;
		public var paused:int = 0;
		public var results:Array = [null, null];
		public var silentlyCancelled:Boolean = false;
		public var chained:Boolean = false;
		public var canceller:Function;

		public function Deferred(canceller:Function=null)
		{
    		this.id = nextId++;
    		this.canceller = canceller;
		}

		public function toString():String
		{
	        var state:String;
	        if (this.fired == -1) {
	            state = 'unfired';
	        } else if (this.fired === 0) {
	            state = 'success';
	        } else {
	            state = 'error';
	        }
	        return 'Deferred(' + this.id + ', ' + state + ')';
		}

		public function cancel():void
		{
	        if (this.fired == -1) {
	            if (this.canceller != null) {
	                this.canceller(this);
	            } else {
	                this.silentlyCancelled = true;
	            }
	            if (this.fired == -1) {
	                this.errback(new CancelledError(this));
	            }
	        } else if ((this.fired === 0) && (this.results[0] is Deferred)) {
	            this.results[0].cancel();
	        }
		}

        /***
        The primitive that means either callback or errback
        ***/
		private function _resback(res:*):void
		{
	        this.fired = ((res is Error) ? 1 : 0);
	        this.results[this.fired] = res;
	        this._fire();
		}

		private function _check():void
		{
	        if (this.fired != -1) {
	            if (!this.silentlyCancelled) {
	                throw new AlreadyCalledError(this);
	            }
	            this.silentlyCancelled = false;
	            return;
	        }
		}

		public function callback(res:*):void
		{
	        this._check();
	        if (res is Deferred) {
	            throw new Error("Deferred instances can only be chained if they are the result of a callback");
	        }
	        this._resback(res);
		}

		public function errback(res:*):void
		{
	        this._check();
	        if (res is Deferred) {
	            throw new Error("Deferred instances can only be chained if they are the result of a callback");
	        }
	        if (!(res is Error)) {
	            res = new GenericError(res);
	        }
	        this._resback(res);
		}

		public function addBoth(fn:Function):Deferred
		{
			return this.addCallbacks(fn, fn);
		}

		public function addCallback(fn:Function):Deferred
		{
			return this.addCallbacks(fn, null);
		}

		public function addErrback(fn:Function):Deferred
		{
			return this.addCallbacks(null, fn);
		}

		public function addCallbacks(cb:Function, eb:Function):Deferred
		{
	        if (this.chained) {
	            throw new Error("Chained Deferreds can not be re-used");
	        }
	        this.chain.push([cb, eb]);
	        if (this.fired >= 0) {
	            this._fire();
	        }
	        return this;
		}

        /***

        Used internally to exhaust the callback sequence when a result
        is available.

        ***/
		private function _fire():void
		{
	        var res:* = this.results[fired];
	        var cb:Function = null;
	        while (chain.length > 0 && this.paused === 0) {
	            // Array
	            var pair:Array = chain.shift();
	            var f:Function = pair[fired];
	            if (f === null) {
	                continue;
	            }
	            try {
	                res = f(res);
	                fired = ((res is Error) ? 1 : 0);
	                if (res is Deferred) {
	                    cb = function (res:*):void {
	                        _resback(res);
	                        paused--;
	                        if ((paused === 0) && (fired >= 0)) {
								_fire();
	                        }
	                    };
	                    this.paused++;
	                }
	            } catch (err:*) {
	                fired = 1;
	                if (!(err is Error)) {
	                    err = new GenericError(err);
	                }
	                res = err;
	            }
	        }
	        this.fired = fired;
	        this.results[fired] = res;
	        if (cb != null && this.paused != 0) {
	            // this is for "tail recursion" in case the dependent deferred
	            // is already fired
	            res.addBoth(cb);
	            res.chained = true;
	        }
		}

		public static function succeed(result:*=null):Deferred
		{
	        var d:Deferred = new Deferred();
	        d.callback(result);
	        return d;
		}

		public static function fail(result:*=null):Deferred
		{
	        var d:Deferred = new Deferred();
	        d.errback(result);
	        return d;
		}

		public static function wait(seconds:Number, value:*=null):Deferred
		{
	        var d:Deferred = new Deferred();

	        if (value != null) {
	            d.addCallback(function(x:*):* { return value; });
	        }

	        var timeout:uint = setTimeout(
	        	function():void { d.callback(null); },
	            Math.floor(seconds * 1000));

	        d.canceller = function (d:Deferred):void {
	            try {
	                clearTimeout(timeout);
	            } catch (e:Error) {
	                // pass
	            }
	        };
	        return d;
		}

		public static function callLater(seconds:Number, func:Function):Deferred
		{
			return wait(seconds).addCallback(function(x:*):*
			{
				return func();
			});
		}

		/**
		 * Generic Adapter from anything to deferreds
		 */
		public static function i(obj:*):Deferred
		{
			if (obj is Deferred)
				return obj;

			if (obj is AsyncToken)
				return new AsyncTokenDeferred(obj);

			if (obj is URLLoader)
				return new URLLoaderDeferred(obj);

			// TODO: More cases

			if (obj is Error)
				return fail(obj);
			return succeed(obj);
		}

	}
}

